package com.culture.config.component;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;

import javax.annotation.Resource;

import com.culture.config.common.util.RandomUtil;
import com.culture.config.constant.ScheduleStatus;
import com.culture.config.dao.entity.JobInfo;
import com.culture.config.dao.mapper.JobMapper;
import com.culture.config.util.HashedWheelTimer;
import com.culture.config.wrapper.JobWrapper;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.support.CronSequenceGenerator;
import org.springframework.stereotype.Component;

import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@EnableScheduling
public class JobScheduleWorker extends HashedWheelTimer 
    implements ApplicationListener<ApplicationEvent>{  
    @Resource
    private JobMapper jobMapper;
    @Autowired
    private JobRunner jobRunner;
    //schedule jobTask Map
    private final Map<String, JobTask> jobTaskMap = new HashMap<>();
   
    public JobScheduleWorker() {
       super(500, TimeUnit.MILLISECONDS, 512);
    }

    public String runOnce(JobInfo jobInfo){
        return this.jobRunner.run(jobInfo);
    }

    public void stopTask(JobInfo jobInfo){
        this.cancelTask(jobInfo);
    }

    private void cancelTask(JobInfo jobInfo){
        JobTask task = this.jobTaskMap.remove(StringUtils.isBlank(jobInfo.getLockKey())?"0":jobInfo.getLockKey());
        if(Objects.nonNull(task)){
            task.setCancelled(true);
            int ret = this.jobMapper.unlock(JobWrapper.wrapJobLockable(jobInfo,task.getTaskId()));
            log.info("[{}] job has been stoped and do unlock:ret:{}", jobInfo.getId(),ret);
            this.removeCancelledTask(task.getTaskKey());
        }        
    }

    private boolean keepLock(JobInfo jobInfo){
        if(StringUtils.isBlank(jobInfo.getLockKey())){
            return false;
        }
        JobTask task = this.jobTaskMap.get(jobInfo.getLockKey());
        if(Objects.isNull(task)){
            return false;
        }           
        if(this.jobMapper.keepLock(JobWrapper.wrapJobLockable(jobInfo,task.getTaskId())) == 1){
            this.jobTaskMap.put(task.getTaskKey(), new JobTask(task.getTaskKey(), jobInfo));
            return true;
        }       
        log.warn("[{}] job keep lock failure.",jobInfo.getId());
        return false;
    }

    private void lockTask(JobInfo jobInfo){
        if(StringUtils.isNotBlank(jobInfo.getLockKey())){
            this.jobTaskMap.remove(jobInfo.getLockKey());
        }
        JobTask task = new JobTask(jobInfo);
        if(this.jobMapper.lock(JobWrapper.wrapJobLockable(jobInfo,task.getTaskId())) == 1){              
            this.jobTaskMap.put(task.getTaskId(),task);
            log.info("add new task:{}",jobInfo.getId());
        }
    }

    public void setupTask(JobInfo jobInfo){
        if(!ScheduleStatus.START.equalById(jobInfo.getScheduleStatus())){
            this.cancelTask(jobInfo);
            return;
        }
        if(this.keepLock(jobInfo)){
           return;
        }
        this.lockTask(jobInfo);
    }

    @Scheduled(fixedDelay = 60000)
    public void jobSchedule(){       
        List<JobInfo> jobList = this.jobMapper.list(JobWrapper.wrapScheduleJobQuery());
        if(CollectionUtils.isEmpty(jobList)){
            return;
        }             
        //schedule job
        jobList.forEach(this::setupTask);
        //start Timer
        this.startTimer();
    }   
  
    @Override
    public List<TimeoutTask> getNewTimeoutTasks(){        
        List<TimeoutTask> timeoutTasks = new ArrayList<>(this.jobTaskMap.size());
        Iterator<Entry<String, JobTask>> it = this.jobTaskMap.entrySet().iterator();
        while(it.hasNext()){
            Entry<String, JobTask> jobTaskEntry = it.next();                
            timeoutTasks.add(jobTaskEntry.getValue());
        }       
        return timeoutTasks;
    }

    @Override
    public void execTimeoutTask(Object taskInfo){    
        if(Objects.isNull(taskInfo)){
            return;
        }
        if(taskInfo instanceof JobInfo){
            this.jobRunner.asyncRun((JobInfo)taskInfo);
        }     
    }     

    @Override
    public void onApplicationEvent(ApplicationEvent event) {       
        if(event instanceof ContextClosedEvent){
            this.stopTimer();
        }        
    }

    @Data
    @Accessors(chain=true)  
    public class JobTask extends TimeoutTask{
        final String taskId;
        final JobInfo jobInfo;              
        final CronSequenceGenerator cronSequenceGenerator; 
        boolean cancelled = false;
       
        public JobTask(JobInfo jobinfo){
            this.taskId = RandomUtil.randomUUID();
            this.jobInfo = jobinfo;    
            this.cronSequenceGenerator = new CronSequenceGenerator(jobInfo.getCron());       
        }

        public JobTask(String taskId,JobInfo jobinfo){
            this.taskId = taskId;
            this.jobInfo = jobinfo;
            this.cronSequenceGenerator = new CronSequenceGenerator(jobInfo.getCron());
        }        

        @Override
        protected boolean isCancelled() {
            return this.cancelled;
        }
       
        @Override
        protected Object getTaskInfo() {
            return this.jobInfo;
        }

        @Override
        protected Long delayWhitMillisecond() { 
            Date nextTime = this.cronSequenceGenerator.next(new Date()); 
            log.info("nextTriggerTime:{}", DateFormatUtils.format(nextTime, "yyyy-MM-dd HH:mm:ss.SSS"));           
            return nextTime.getTime() - System.currentTimeMillis();
        }

        @Override
        protected String getTaskKey() {
            return this.taskId;
        }  
    }
   
}
